﻿using System;
using System.Collections.Generic;
using System.Text;
using RoughJs.Ast;

namespace RoughJs.E4X
{
    /// <summary>
    /// 2010-01-5 ----- 2010-01-29
    /// 陈曦 1.0
    /// Microsoft Public License (Ms-PL)
    ///This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
    ///1. Definitions
    ///The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
    ///A "contribution" is the original software, or any additions or changes to the software.
    ///A "contributor" is any person that distributes its contribution under this license.
    ///"Licensed patents" are a contributor's patent claims that read directly on its contribution.
    ///2. Grant of Rights
    ///(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
    ///(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
    ///3. Conditions and Limitations
    ///(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
    ///(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
    ///(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
    ///(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
    ///(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
    /// </summary>
    public class XMLLibImpl : XMLLib
    {
        private QName qnamePrototype1;

        private XmlProcessor options = new XmlProcessor();
        private Namespace namespacePrototype;
        private XMLList xmlListPrototype;
        private Scriptable globalScope1;
        private XML xmlPrototype;
        public static void init(Context cx, Scriptable scope, bool sealed1)
        {
            XMLLibImpl lib = new XMLLibImpl(scope);
            XMLLib bound = lib.bindToScope(scope);
            if (bound == lib)
            {
                lib.exportToScope(sealed1);
            }
        }
        private XMLLibImpl(Scriptable globalScope)
        {
            this.globalScope1 = globalScope;
        }
        public XML newXML(XmlNode node)
        {
            return new XML(this, this.globalScope1, this.xmlPrototype, node);
        }

        public void setIgnoreComments(bool b)
        {
            options.setIgnoreComments(b);
        }

        public void setIgnoreWhitespace(bool b)
        {
            options.setIgnoreWhitespace(b);
        }

        public void setIgnoreProcessingInstructions(bool b)
        {
            options.setIgnoreProcessingInstructions(b);
        }

        public void setPrettyPrinting(bool b)
        {
            options.setPrettyPrinting(b);
        }

        public void setPrettyIndent(int i)
        {
            options.setPrettyIndent(i);
        }

        public bool isIgnoreComments()
        {
            return options.isIgnoreComments();
        }

        public bool isIgnoreProcessingInstructions()
        {
            return options.isIgnoreProcessingInstructions();
        }

        public bool isIgnoreWhitespace()
        {
            return options.isIgnoreWhitespace();
        }

        public bool isPrettyPrinting()
        {
            return options.isPrettyPrinting();
        }

        public int getPrettyIndent()
        {
            return options.getPrettyIndent();
        }

        /** @deprecated */
        public QName qnamePrototype()
        {
            return qnamePrototype1;
        }

        /** @deprecated */
        public Scriptable globalScope()
        {
            return globalScope1;
        }

        public XmlProcessor getProcessor()
        {
            return options;
        }

        private void exportToScope(bool sealed1)
        {
            xmlPrototype = newXML(XmlNode.createText(options, ""));
            xmlListPrototype = newXMLList();
            namespacePrototype = Namespace.create(this.globalScope1, null, XmlNode.Namespace.GLOBAL);
            qnamePrototype1 = QName.create(this, this.globalScope1, null, XmlNode.QName.create(XmlNode.Namespace.create(""), ""));

            xmlPrototype.exportAsJSClass(sealed1);
            xmlListPrototype.exportAsJSClass(sealed1);
            namespacePrototype.exportAsJSClass(sealed1);
            qnamePrototype1.exportAsJSClass(sealed1);
        }



        public XMLList newXMLList()
        {
            return new XMLList(this, this.globalScope1, this.xmlListPrototype);
        }



        public XMLList newXMLListFrom(Object inputObject)
        {
            XMLList rv = newXMLList();

            if (inputObject == null || inputObject is Undefined)
            {
                return rv;
            }
            else if (inputObject is XML)
            {
                XML xml = (XML)inputObject;
                rv.getNodeList().add(xml);
                return rv;
            }
            else if (inputObject is XMLList)
            {
                XMLList xmll = (XMLList)inputObject;
                rv.getNodeList().add(xmll.getNodeList());
                return rv;
            }
            else
            {
                String frag = ScriptRuntime.toString(inputObject).Trim();

                if (!frag.StartsWith("<>"))
                {
                    frag = "<>" + frag + "</>";
                }

                frag = "<fragment>" + frag.Substring(2);
                if (!frag.EndsWith("</>"))
                {
                    throw ScriptRuntime.typeError("XML with anonymous tag missing end anonymous tag");
                }

                frag = frag.Substring(0, frag.Length - 3) + "</fragment>";

                XML orgXML = newXMLFromJs(frag);

                // Now orphan the children and add them to our XMLList.
                XMLList children = orgXML.children();

                for (int i = 0; i < children.getNodeList().length(); i++)
                {
                    // Copy here is so that they'll be orphaned (parent() will be undefined)
                    rv.getNodeList().add(((XML)children.item(i).copy()));
                }
                return rv;
            }
        }

        public XML newXMLFromJs(Object inputObject)
        {
            String frag;

            if (inputObject == null || inputObject == Undefined.instance)
            {
                frag = "";
            }
            else if (inputObject is XMLObjectImpl)
            {
                // todo: faster way for XMLObjects?
                frag = ((XMLObjectImpl)inputObject).toXMLString();
            }
            else
            {
                frag = ScriptRuntime.toString(inputObject);
            }

            if (frag.Trim().StartsWith("<>"))
            {
                throw ScriptRuntime.typeError("Invalid use of XML object anonymous tags <></>.");
            }

            if (frag.IndexOf("<") == -1)
            {
                //    Solo text node
                return newXML(XmlNode.createText(options, frag));
            }
            return parse(frag);
        }

        //private XML parse(String frag)
        //{
        //    try
        //    {
        //        return newXML(System.Xml.XmlNode.CreateElement(options, getDefaultNamespaceURI(Context.getCurrentContext()), frag));
        //    }
        //    catch (Exception e)
        //    {
        //        throw ScriptRuntime.typeError("Cannot parse XML: " + e.Message);
        //    }
        //}
        //private String getDefaultNamespaceURI(Context cx)
        //{
        //    return getDefaultNamespace(cx).uri();
        //}
        public Namespace newNamespace(String uri)
        {
            return this.namespacePrototype.newNamespace(uri);
        }


        public Namespace getDefaultNamespace(Context cx)
        {
            if (cx == null)
            {
                cx = Context.getCurrentContext();
                if (cx == null)
                {
                    return namespacePrototype;
                }
            }

            Object ns = ScriptRuntime.searchDefaultNamespace(cx);
            if (ns == null)
            {
                return namespacePrototype;
            }
            else
            {
                if (ns is Namespace)
                {
                    return (Namespace)ns;
                }
                else
                {
                    //    TODO    Clarify or remove the following comment
                    // Should not happen but for now it could
                    // due to bad searchDefaultNamespace implementation.
                    return namespacePrototype;
                }
            }
        }


        public XMLName toXMLNameFromString(Context cx, String name)
        {
            return XMLName.create(getDefaultNamespaceURI(cx), name);
        }

        /* TODO: Marked deprecated by original author */
        public XMLName toXMLName(Context cx, Object nameValue)
        {
            XMLName result;

            if (nameValue is XMLName)
            {
                result = (XMLName)nameValue;
            }
            else if (nameValue is QName)
            {
                QName qname = (QName)nameValue;
                result = XMLName.formProperty(qname.uri(), qname.localName());
            }
            else if (nameValue is String)
            {
                result = toXMLNameFromString(cx, (String)nameValue);
            }
            else if (nameValue is Boolean
              || ScriptRuntime.isNumber(nameValue)
              || nameValue == Undefined.instance
              || nameValue == null)
            {
                throw badXMLName(nameValue);
            }
            else
            {
                String name = ScriptRuntime.toString(nameValue);
                result = toXMLNameFromString(cx, name);
            }

            return result;
        }

        private static Exception badXMLName(Object value)
        {
            String msg;
            if (ScriptRuntime.isNumber(value))
            {
                msg = "Can not construct XML name from number: ";
            }
            else if (value is Boolean)
            {
                msg = "Can not construct XML name from boolean: ";
            }
            else if (value == Undefined.instance || value == null)
            {
                msg = "Can not construct XML name from ";
            }
            else
            {
                throw new ArgumentException(value.ToString());
            }
            return ScriptRuntime.typeError(msg + ScriptRuntime.toString(value));
        }

        ///**
        // * If value represents Uint32 index, make it available through
        // * ScriptRuntime.lastUint32Result(cx) and return null.
        // * Otherwise return the same value as toXMLName(cx, value).
        // */
        //XMLName toXMLNameOrIndex(Context cx, Object value)
        //{
        //    XMLName result;

        //    if (value is XMLName)
        //    {
        //        result = (XMLName)value;
        //    }
        //    else if (value is String)
        //    {
        //        String str = (String)value;
        //        long test = ScriptRuntime.testUint32String(str);
        //        if (test >= 0)
        //        {
        //            ScriptRuntime.storeUint32Result(cx, test);
        //            result = null;
        //        }
        //        else
        //        {
        //            result = toXMLNameFromString(cx, str);
        //        }
        //    }
        //    else if (ScriptRuntime.isNumber(value))
        //    {
        //        double d = ScriptRuntime.toNumber(value);
        //        long l = (long)d;
        //        if (l == d && 0 <= l && l <= 0xFFFFFFFFL)
        //        {
        //            ScriptRuntime.storeUint32Result(cx, l);
        //            result = null;
        //        }
        //        else
        //        {
        //            throw badXMLName(value);
        //        }
        //    }
        //    else if (value is QName)
        //    {
        //        QName qname = (QName)value;
        //        String uri = qname.uri();
        //        bool number = false;
        //        result = null;
        //        if (uri != null && uri.Length == 0)
        //        {
        //            // Only in this case qname.toString() can resemble uint32
        //            long test = ScriptRuntime.testUint32String(uri);
        //            if (test >= 0)
        //            {
        //                ScriptRuntime.storeUint32Result(cx, test);
        //                number = true;
        //            }
        //        }
        //        if (!number)
        //        {
        //            result = XMLName.formProperty(uri, qname.localName());
        //        }
        //    }
        //    else if (value is Boolean
        //             || value == Undefined.instance
        //             || value == null)
        //    {
        //        throw badXMLName(value);
        //    }
        //    else
        //    {
        //        String str = ScriptRuntime.toString(value);
        //        long test = ScriptRuntime.testUint32String(str);
        //        if (test >= 0)
        //        {
        //            ScriptRuntime.storeUint32Result(cx, test);
        //            result = null;
        //        }
        //        else
        //        {
        //            result = toXMLNameFromString(cx, str);
        //        }
        //    }

        //    return result;
        //}

        public Object addXMLObjects(Context cx, XMLObject obj1, XMLObject obj2)
        {
            XMLList listToAdd = newXMLList();

            if (obj1 is XMLList)
            {
                XMLList list1 = (XMLList)obj1;
                if (list1.length() == 1)
                {
                    listToAdd.addToList(list1.item(0));
                }
                else
                {
                    // Might be xmlFragment + xmlFragment + xmlFragment + ...;
                    // then the result will be an XMLList which we want to be an
                    // rValue and allow it to be assigned to an lvalue.
                    listToAdd = newXMLListFrom(obj1);
                }
            }
            else
            {
                listToAdd.addToList(obj1);
            }

            if (obj2 is XMLList)
            {
                XMLList list2 = (XMLList)obj2;
                for (int i = 0; i < list2.length(); i++)
                {
                    listToAdd.addToList(list2.item(i));
                }
            }
            else if (obj2 is XML)
            {
                listToAdd.addToList(obj2);
            }

            return listToAdd;
        }

        private Ref xmlPrimaryReference(Context cx, XMLName xmlName, Scriptable scope)
        {
            XMLObjectImpl xmlObj;
            XMLObjectImpl firstXml = null;
            for (; ; )
            {
                // XML object can only present on scope chain as a wrapper
                // of XMLWithScope
                if (scope is XMLWithScope)
                {
                    xmlObj = (XMLObjectImpl)scope.getPrototype();
                    if (xmlObj.hasXMLProperty(xmlName))
                    {
                        break;
                    }
                    if (firstXml == null)
                    {
                        firstXml = xmlObj;
                    }
                }
                scope = scope.getParentScope();
                if (scope == null)
                {
                    xmlObj = firstXml;
                    break;
                }
            }

            // xmlObj == null corresponds to undefined as the target of
            // the reference
            if (xmlObj != null)
            {
                xmlName.initXMLObject(xmlObj);
            }
            return xmlName;
        }

        public Namespace castToNamespace(Context cx, Object namespaceObj)
        {
            return this.namespacePrototype.castToNamespace(namespaceObj);
        }

        private String getDefaultNamespaceURI(Context cx)
        {
            return getDefaultNamespace(cx).uri();
        }


        public Namespace[] createNamespaces(XmlNode.Namespace[] declarations)
        {
            Namespace[] rv = new Namespace[declarations.Length];
            for (int i = 0; i < declarations.Length; i++)
            {
                rv[i] = this.namespacePrototype.newNamespace(declarations[i].getPrefix(), declarations[i].getUri());
            }
            return rv;
        }

        //    See ECMA357 13.3.2
        public QName constructQName(Context cx, Object namespace1, Object name)
        {
            return this.qnamePrototype1.constructQName(this, cx, namespace1, name);
        }

        public QName newQName(String uri, String localName, String prefix)
        {
            return this.qnamePrototype1.newQName(this, uri, localName, prefix);
        }

        public QName constructQName(Context cx, Object nameValue)
        {
            //        return constructQName(cx, Undefined.instance, nameValue);
            return this.qnamePrototype1.constructQName(this, cx, nameValue);
        }

        public QName castToQName(Context cx, Object qnameValue)
        {
            return this.qnamePrototype1.castToQName(this, cx, qnameValue);
        }

        public QName newQName(XmlNode.QName qname)
        {
            return QName.create(this, this.globalScope1, this.qnamePrototype1, qname);
        }



        private XML parse(String frag)
        {
            try
            {
                return newXML(XmlNode.createElement(options, getDefaultNamespaceURI(Context.getCurrentContext()), frag));
            }
            catch (Exception e)
            {
                throw ScriptRuntime.typeError("Cannot parse XML: " + e.Message);
            }
        }

        public XML ecmaToXml(Object object1)
        {
            //    See ECMA357 10.3
            if (object1 == null || object1 == Undefined.instance) throw ScriptRuntime.typeError("Cannot convert " + object1 + " to XML");
            if (object1 is XML) return (XML)object1;
            if (object1 is XMLList)
            {
                XMLList list = (XMLList)object1;
                if (list.getXML() != null)
                {
                    return list.getXML();
                }
                else
                {
                    throw ScriptRuntime.typeError("Cannot convert list of >1 element to XML");
                }
            }
            //    TODO    Technically we should fail on anything except a String, Number or Boolean
            //            See ECMA357 10.3
            // Extension: if object is a DOM node, use that to construct the XML
            // object.
            if (object1 is Wrapper)
            {
                object1 = ((Wrapper)object1).unwrap();
            }
            if (object1 is System.Xml.XmlNode)
            {
                System.Xml.XmlNode node = (System.Xml.XmlNode)object1;
                return newXML(XmlNode.createElementFromNode(node));
            }
            //    Instead we just blindly cast to a String and let them convert anything.
            String s = ScriptRuntime.toString(object1);
            //    TODO    Could this get any uglier?
            if (s.Length > 0 && s.ToCharArray()[0] == '<')
            {
                return parse(s);
            }
            else
            {
                return newXML(XmlNode.createText(options, s));
            }
        }

        public XML newTextElementXML(XmlNode reference, XmlNode.QName qname, String value)
        {
            return newXML(XmlNode.newElementWithText(options, reference, qname, value));
        }





        public XmlNode.QName toNodeQName(Context cx, Object namespaceValue, Object nameValue)
        {
            // This is duplication of constructQName(cx, namespaceValue, nameValue)
            // but for XMLName

            String localName;

            if (nameValue is QName)
            {
                QName qname = (QName)nameValue;
                localName = qname.localName();
            }
            else
            {
                localName = ScriptRuntime.toString(nameValue);
            }

            XmlNode.Namespace ns;
            if (namespaceValue == Undefined.instance)
            {
                if ("*" == localName)
                {
                    ns = null;
                }
                else
                {
                    ns = getDefaultNamespace(cx).getDelegate();
                }
            }
            else if (namespaceValue == null)
            {
                ns = null;
            }
            else if (namespaceValue is Namespace)
            {
                ns = ((Namespace)namespaceValue).getDelegate();
            }
            else
            {
                ns = this.namespacePrototype.constructNamespace(namespaceValue).getDelegate();
            }

            if (localName != null && localName == "*") localName = null;
            return XmlNode.QName.create(ns, localName);
        }

        public XmlNode.QName toNodeQName(Context cx, String name, bool attribute)
        {
            XmlNode.Namespace defaultNamespace = getDefaultNamespace(cx).getDelegate();
            if (name != null && name == "*")
            {
                return XmlNode.QName.create(null, null);
            }
            else
            {
                if (attribute)
                {
                    return XmlNode.QName.create(XmlNode.Namespace.GLOBAL, name);
                }
                else
                {
                    return XmlNode.QName.create(defaultNamespace, name);
                }
            }
        }

        /*
            TODO: Too general; this should be split into overloaded methods.
            Is that possible?
         */
        public XmlNode.QName toNodeQName(Context cx, Object nameValue, bool attribute)
        {
            if (nameValue is XMLName)
            {
                return ((XMLName)nameValue).toQname();
            }
            else if (nameValue is QName)
            {
                QName qname = (QName)nameValue;
                return qname.getDelegate();
            }
            else if (
              nameValue is Boolean
              || ScriptRuntime.isNumber(nameValue)
              || nameValue == Undefined.instance
              || nameValue == null
          )
            {
                throw badXMLName(nameValue);
            }
            else
            {
                String local = null;
                if (nameValue is String)
                {
                    local = (String)nameValue;
                }
                else
                {
                    local = ScriptRuntime.toString(nameValue);
                }
                return toNodeQName(cx, local, attribute);
            }
        }

        //
        //    Override methods from XMLLib
        //


        public override bool isXMLName(Context _cx, Object nameObj)
        {
            return XMLName.accept(nameObj);
        }


        public override Object toDefaultXmlNamespace(Context cx, Object uriValue)
        {
            return this.namespacePrototype.constructNamespace(uriValue);
        }


        public override String escapeTextValue(Object o)
        {
            return options.escapeTextValue(o);
        }


        public override String escapeAttributeValue(Object o)
        {
            return options.escapeAttributeValue(o);
        }


        public override Ref nameRef(Context cx, Object name, Scriptable scope, int memberTypeFlags)
        {
            if ((memberTypeFlags & Node.ATTRIBUTE_FLAG) == 0)
            {
                // should only be called for cases like @name or @[expr]
                throw Kit.codeBug();
            }
            XMLName xmlName = toAttributeName(cx, name);
            return xmlPrimaryReference(cx, xmlName, scope);
        }


        public override Ref nameRef(Context cx, Object namespace1, Object name, Scriptable scope, int memberTypeFlags)
        {
            XMLName xmlName = XMLName.create(toNodeQName(cx, namespace1, name), false, false);

            //    No idea what is coming in from the parser in this case; is it detecting the "@"?
            if ((memberTypeFlags & Node.ATTRIBUTE_FLAG) != 0)
            {
                if (!xmlName.isAttributeName())
                {
                    xmlName.setAttributeName();
                }
            }

            return xmlPrimaryReference(cx, xmlName, scope);
        }
        public XMLName toAttributeName(Context cx, Object nameValue)
        {
            if (nameValue is XMLName)
            {
                //    TODO    Will this always be an XMLName of type attribute name?
                return (XMLName)nameValue;
            }
            else if (nameValue is QName)
            {
                return XMLName.create(((QName)nameValue).getDelegate(), true, false);
            }
            else if (nameValue is Boolean
              || ScriptRuntime.isNumber(nameValue)
              || nameValue == Undefined.instance
              || nameValue == null)
            {
                throw badXMLName(nameValue);
            }
            else
            {
                //    TODO    Not 100% sure that putting these in global namespace is the right thing to do
                String localName = null;
                if (nameValue is String)
                {
                    localName = (String)nameValue;
                }
                else
                {
                    localName = ScriptRuntime.toString(nameValue);
                }
                if (localName != null && (localName == "*")) localName = null;
                return XMLName.create(XmlNode.QName.create(XmlNode.Namespace.create(""), localName), true, false);
            }
        }


        public XMLName toXMLNameOrIndex(Context cx, Object value)
        {
            XMLName result;

            if (value is XMLName)
            {
                result = (XMLName)value;
            }
            else if (value is String)
            {
                String str = (String)value;
                long test = ScriptRuntime.testUint32String(str);
                if (test >= 0)
                {
                    ScriptRuntime.storeUint32Result(cx, test);
                    result = null;
                }
                else
                {
                    result = toXMLNameFromString(cx, str);
                }
            }
            else if (ScriptRuntime.isNumber(value))
            {
                double d = ScriptRuntime.toNumber(value);
                long l = (long)d;
                if (l == d && 0 <= l && l <= 0xFFFFFFFFL)
                {
                    ScriptRuntime.storeUint32Result(cx, l);
                    result = null;
                }
                else
                {
                    throw badXMLName(value);
                }
            }
            else if (value is QName)
            {
                QName qname = (QName)value;
                String uri = qname.uri();
                bool number = false;
                result = null;
                if (uri != null && uri.Length == 0)
                {
                    // Only in this case qname.toString() can resemble uint32
                    long test = ScriptRuntime.testUint32String(uri);
                    if (test >= 0)
                    {
                        ScriptRuntime.storeUint32Result(cx, test);
                        number = true;
                    }
                }
                if (!number)
                {
                    result = XMLName.formProperty(uri, qname.localName());
                }
            }
            else if (value is Boolean
                     || value == Undefined.instance
                     || value == null)
            {
                throw badXMLName(value);
            }
            else
            {
                String str = ScriptRuntime.toString(value);
                long test = ScriptRuntime.testUint32String(str);
                if (test >= 0)
                {
                    ScriptRuntime.storeUint32Result(cx, test);
                    result = null;
                }
                else
                {
                    result = toXMLNameFromString(cx, str);
                }
            }

            return result;
        }

    }// end for class
}
